﻿<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>displacement</title>


</head>
<body>

<script id="draw-shader-vs" type="x-shader/x-vertex">
precision highp float;

attribute vec3 inPos;
attribute vec3 inTV;
attribute vec3 inBV;
attribute vec3 inNV;
attribute vec2 inUV;
attribute vec3 inCol;

varying vec3 vertPos;
varying vec3 vertTV;
varying vec3 vertBV;
varying vec3 vertNV;
varying vec2 vertUV;
varying vec3 vertCol;

uniform mat4  u_projectionMat44;
uniform mat4  u_viewMat44;
uniform mat4  u_modelMat44;

uniform sampler2D u_vertDispMap;
uniform float     u_vertDisplScale;

void main()
{   
    float height  = texture2D(u_vertDispMap, inUV).x * u_vertDisplScale;

    vertCol       = inCol;
    vertUV        = inUV;

    mat4 mv       = u_viewMat44 * u_modelMat44; 
    mat3 tbn      = mat3(mv) * mat3(inTV, inBV, inNV);

    vertTV        = normalize(tbn[0]);
    vertBV        = normalize(tbn[1]);
    vertNV        = normalize(tbn[2]);
    
    vec4 viewPos  = mv * vec4( inPos, 1.0 );
    viewPos.xyz  += normalize(vertNV) * height;
    vertPos       = viewPos.xyz;
    gl_Position   = u_projectionMat44 * viewPos;
}
</script>

<script id="draw-shader-fs" type="x-shader/x-fragment">
//#define NORMAL_MAP_TEXTURE
#define NORMAL_MAP_QUALITY 0
#define TANGENT_BINORMAL

#if !defined(TANGENT_BINORMAL)
#extension GL_OES_standard_derivatives : enable
#endif

precision mediump float;

varying vec3 vertPos;
varying vec3 vertTV;
varying vec3 vertBV;
varying vec3 vertNV;
varying vec2 vertUV;
varying vec3 vertCol;

uniform sampler2D u_texture;
uniform sampler2D u_displacement_map;
uniform vec2      u_displacement_map_size;
uniform float     u_displacement_scale;
uniform float     u_mix_color;
uniform float     u_mix_normal;
uniform float     u_height;

float CalculateHeight( in vec2 texCoords )
{
    float height = texture2D( u_displacement_map, texCoords ).x;
    return clamp( height, 0.0, 1.0 );
}

vec4 CalculateNormal( in vec2 texCoords )
{
#if defined(NORMAL_MAP_TEXTURE)
    float height = CalculateHeight( texCoords );
    vec3  tempNV = texture2D( u_normal_map, texCoords ).xyz * 2.0 / 1.0;
    return vec4( normalize( tempNV ), height );
#else
    vec2 texOffs = 1.0 / u_displacement_map_size;
    vec2 scale   = 1.0 / texOffs;
#if NORMAL_MAP_QUALITY > 0
    float hx[9];
    hx[0] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2(-1.0, -1.0) ).r;
    hx[1] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 0.0, -1.0) ).r;
    hx[2] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 1.0, -1.0) ).r;
    hx[3] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2(-1.0,  0.0) ).r;
    hx[4] = texture2D( u_displacement_map, texCoords.st ).r;
    hx[5] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 1.0, 0.0) ).r;
    hx[6] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2(-1.0, 1.0) ).r;
    hx[7] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 0.0, 1.0) ).r;
    hx[8] = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 1.0, 1.0) ).r;
    vec2  deltaH = vec2(hx[0]-hx[2] + 2.0*(hx[3]-hx[5]) + hx[6]-hx[8], hx[0]-hx[6] + 2.0*(hx[1]-hx[7]) + hx[2]-hx[8]);
    float h_mid  = hx[4];
#else
    float h_mid  = texture2D( u_displacement_map, texCoords.st ).r;
    float h_xa   = texture2D( u_displacement_map, texCoords.st + texOffs * vec2(-1.0,  0.0) ).r;
    float h_xb   = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 1.0,  0.0) ).r;
    float h_ya   = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 0.0, -1.0) ).r;
    float h_yb   = texture2D( u_displacement_map, texCoords.st + texOffs * vec2( 0.0,  1.0) ).r;
    vec2  deltaH = vec2(h_xa-h_xb, h_ya-h_yb);
#endif
    return vec4( normalize( vec3( deltaH * scale, 1.0 ) ), h_mid );
#endif
}


// light model uniforms
uniform vec4  u_lightPosition;
uniform vec4  u_lightColor;
uniform float u_ambient;
uniform float u_diffuse;
uniform float u_specular;
uniform float u_specular_tint;
uniform float u_roughness;
uniform float u_metallic;
uniform float u_sheen;
uniform float u_sheen_tint;
uniform float u_subsurface;
uniform float u_clear_coat;
uniform float u_clear_coat_gloss;
uniform float u_anisotropic;

const float PI = 3.14159265358979323846;

float Fresnel_Schlick( float theta )
{
    float m = clamp( 1.0 - theta, 0.0, 1.0 );
    float m2 = m * m;
    return m2 * m2 * m; // pow( m, 5.0 )
}

float GTR_Berrey( float NdotH, float al ) // GTR gamma=1
{
    if ( al >= 1.0 ) return 1.0 / PI;
    float al2 = al * al;
    float t = 1.0 + ( al2 - 1.0 ) * NdotH * NdotH;
    return ( al2 - 1.0 ) / ( PI * log( al2 ) * t );
}

float GTR_2_anisotropic( float NdotH, float HdotX, float HdotY, float alx, float aly )
{
    float tx = HdotX / alx;
    float ty = HdotY / aly;
    float t = tx * tx + ty * ty + NdotH * NdotH;
    return 1.0 / ( PI * alx * aly * t * t );
}

float G_Schlick( float cosTheta, float m )
{
    float k = m * 0.7979; // 0.7979 ~ sqrt( 2.0 / 3.14159 );
    return cosTheta / ( cosTheta * ( 1.0 - k ) + k );
}

float G_GGX_anisotropic( float NdotV, float VdotX, float VdotY, float alx, float aly )
{
    float tx = VdotX * alx;
    float ty = VdotY / aly;
    return 1.0 / ( NdotV + sqrt( tx * tx + ty * ty + NdotV * NdotV ) );
}

vec3 LightModel(vec3 esPt, vec3 TV, vec3 BV, vec3 esPtNV)
{
    vec3 ambientCol = u_lightColor.rgb * u_ambient;
    vec3 diffuseCol = u_lightColor.rgb;
    vec3 specularCol = u_lightColor.rgb;
    vec3 finalLightColor = ambientCol;
    
    vec3 esVEye = normalize(-esPt);
    vec3 vec_X  = normalize( TV );
    vec3 vec_Y  = normalize( BV );
  
    float m2            = u_roughness * u_roughness;
    float NdotV         = dot(esPtNV, esVEye);
    float NdotV_clamped = clamp(NdotV, 0.0, 1.0);
    float VdotX         = dot(esVEye, vec_X);
    float VdotY         = dot(esVEye, vec_Y);
    float F_NdotV       = Fresnel_Schlick(NdotV);
    NdotV = clamp(NdotV, 0.0, 1.0);

    vec3  esVLight   = u_lightPosition.xyz;
    float localLight = u_lightPosition.w; 

    esVLight = esVLight - esPt * localLight;
    float lightDist = length(esVLight);
    esVLight /= lightDist;
    
    float lightIntensity = 1.0;
    if (localLight != 0.0)
    {
        // TODO $$$ spotlight, attenuation
    }

    float NdotL = dot(esPtNV, esVLight);
    float VdotL = dot(esVEye, esVLight);
    
    vec3 halfVector = normalize(esVEye + esVLight);
    float NdotH = dot(esPtNV, halfVector);
    float HdotL = dot(halfVector, esVLight); // == VdotH == dot( esVEye, halfVector )
    
    float LdotX = dot(esVLight, vec_X);
    float LdotY = dot(esVLight, vec_Y);
    float HdotX = dot(halfVector, vec_X);
    float HdotY = dot(halfVector, vec_Y);
    
    float FSS90 = HdotL * HdotL * u_roughness;
    float FD90 = 0.5 + 2.0 * FSS90;
    float F_NdotL = Fresnel_Schlick(NdotL);
    float F_HdotL = Fresnel_Schlick(HdotL);
    float err_d = step(0.0, NdotL) + step(NdotL, 0.0) * pow(1.0 + NdotL, 5.0);
    float err_s = err_d;
    NdotL = abs(NdotL);
    
    float k_diffuse = max(0.0, NdotL);
    
    // luminance approximation
    float spec_lum     = specularCol.r * 0.3 + specularCol.g * 0.6 + specularCol.b * 0.1;
    vec3 col_spec_tint = spec_lum > 0.0 ? specularCol.rgb / spec_lum : vec3(1.0); // normalize luminance to isolate hue+sat
    vec3 col_spec      = mix(vec3(1.0), col_spec_tint, u_specular_tint);  
    vec3 col_spec_0    = mix(u_specular * 0.08 * col_spec, specularCol.rgb, u_metallic);
    vec3 col_sheen     = mix(vec3(1.0), col_spec_tint, u_sheen_tint);
    // diffuse energy ratio Burley at Disney
    //float Fd           = mix(1.0, FD90, F_NdotL) * mix(1.0, FD90, F_NdotV);
    float Fd           = (1.0 + (1.0 - FD90) * F_NdotL) * (1.0 + (1.0 - FD90) * F_NdotV); // ??? similar but not identically : FD90 + (1.0 - FD90) * F_NdotL
    // Based on Hanrahan-Krueger BRDF approximation of isotropic BSSRDF
    // 1.25 scale is used to (roughly) preserve albedo
    // FSS90 used to "flatten" retroreflection based on roughness
    //float Fss          = mix(1.0, FSS90, F_NdotL) * mix(1.0, FSS90, F_NdotV);
    float Fss          = (1.0 + (1.0 - FSS90) * F_NdotL) * (1.0 + (1.0 - FSS90) * F_NdotV); // ??? similar but not identically
    float ss           = 1.25 * (Fss * (1.0 / (NdotL + NdotV + 0.0001) - 0.5) + 0.5);
    // sheen
    vec3 Fsheen        = F_HdotL * u_sheen * col_sheen;
    // diffuse material color
    vec3 diff_mat     = u_diffuse * k_diffuse * diffuseCol.rgb + ((1.0/PI) * mix(Fd, ss, u_subsurface) * diffuseCol.rgb + Fsheen) * (1.0 - u_metallic);
    
    vec3 Fs      = col_spec_0 + (1.0 - col_spec_0) * F_HdotL;
    float m_ggx  = (0.5 + u_roughness / 2.0);
    float m2_ggx = m_ggx * m_ggx;
    
    float an_val    = (u_anisotropic - 0.5) * 1.98; // anisotropic from range [0.0, 1.0] -> [-0.99, 0.99]
    float aspect    = sqrt(1.0 - abs(an_val)); 
    vec2  aspect_xy = an_val < 0.0 ? vec2(aspect, 1.0 / aspect) : vec2(1.0 / aspect, aspect);
    vec2  al_m2     = vec2(max(0.00001, m2 * aspect_xy.x), max(0.00001, m2 * aspect_xy.y));
    vec2  al_ggx    = vec2(max(0.00001, m2_ggx * aspect_xy.x), max(0.00001, m2_ggx * aspect_xy.y));
    float Ds        = GTR_2_anisotropic(NdotH, HdotX, HdotY, al_m2.x, al_m2.y);
    float Gs        = G_GGX_anisotropic(NdotL, LdotX, LdotY, al_ggx.x, al_ggx.y) * G_GGX_anisotropic(NdotV, VdotX, VdotY, al_ggx.x, al_ggx.y);
    
    // clearcoat (ior = 1.5 -> F0 = 0.04)
    float Dr       = GTR_Berrey(NdotH, mix(0.1, 0.001, u_clear_coat_gloss));
    float Fr       = mix(0.04, 1.0, F_HdotL);
    float Gr       = G_Schlick(NdotL, 0.25) * G_Schlick(NdotV, 0.25);
    // specualr material color
    vec3 spec_mat = Gs * Fs * Ds + 0.25 * u_clear_coat * Gr * Fr * Dr;

    finalLightColor += err_d * diff_mat;
    finalLightColor += err_s * spec_mat;
      
    return finalLightColor;
} 


mat3 TBN( vec3 N)
{
    float face_sign   = sign(dot(vertNV, -vertPos));
#if defined(TANGENT_BINORMAL)
    mat3  tbnMat      = mat3(vertTV, vertBV, N * face_sign);
#else
    vec3  dp1         = dFdx( vertPos );
    vec3  dp2         = dFdy( vertPos );
    vec2  duv1        = dFdx( vertUV );
    vec2  duv2        = dFdy( vertUV );
    vec3  dp2perp     = cross(dp2, N);
    vec3  dp1perp     = cross(N, dp1);
    vec3  T           = dp2perp * duv1.x + dp1perp * duv2.x;
    vec3  B           = dp2perp * duv1.y + dp1perp * duv2.y;
    float invmax      = inversesqrt(max(dot(T, T), dot(B, B)));
    mat3  tbnMat      = mat3(T * invmax, B * invmax, N * face_sign);
#endif
    return tbnMat;
}


void main()
{
    vec3 N          = normalize(vertNV);
    mat3 tbnMat     = TBN(N); 
    vec4 normalVec  = CalculateNormal( vertUV );
    normalVec.xy   *= u_displacement_scale;
    vec3 nvMappedEs = normalize( tbnMat * normalVec.xyz );

    vec3 lightColor = LightModel(vertPos, vertTV, vertBV, nvMappedEs);

    vec4 texColor   = texture2D(u_texture, vertUV);
    vec3 view_nv    = normalize(nvMappedEs);
    vec3 nv_color   = abs(view_nv); 
    nv_color       /= max(nv_color.x, max(nv_color.y, nv_color.z));
    vec3 baseColor  = mix(mix(vertCol, texColor.rgb, u_mix_color), nv_color, u_mix_normal);
    
    vec3 color      = baseColor * lightColor;
    gl_FragColor    = vec4( color.rgb, 1.0 );
} 
</script>

<style>
html,body { margin: 0; overflow: hidden; }
#main-view { position: relative; }
#gui-left { position: absolute; top: 0; left: 0; color: #40f040; font-size : large; }
#gui-right { position: absolute; top: 0; right: 0; color: #d0d040; font-size : large; }
#gui-bottom { position: absolute; bottom: 0; left: 0; font-size : large; }  
#instruction { color: #CCF; font-size : large; }
#ref-link { position: absolute; bottom: 0; left: 0; font-size : large; }  
</style>

<div id="main-view">

<div id="gui-left"><form name="inputs">
    <table>
    <tr><td>form</td> <td>
        <select id="mesh">
            <option value="0">tetrahedron</option>
            <option value="1">cube</option>
            <option value="2">icosahedron</option>
            <option value="3">tube</option>
            <option value="4">cone</option>
            <option value="5">sphere sliced</option>
            <option value="6">sphere tessellated</option>
            <option value="7">torus</option>
            <option value="8" selected="selected">trefoil knot</option>
            <option value="9">torus knot</option>
        </select>
    </td> </tr>
    <tr><td>field of view</td><td><input type="range" id="fov" min="0" max="100" value="70"/></td></tr>
    <tr><td>color / texture</td><td><input type="range" id="mix_color" min="0" max="100" value="0"/></td></tr>
    <tr><td>displacement height</td><td><input type="range" id="height" min="0" max="100" value="5"/></td></tr>
    <tr><td>tessellation</td><td><input type="range" id="tessellation" min="10" max="100" value="25"/></td></tr>
    </table>
    <span id="info">:</span>
</form></div>

<div id="gui-right"><form name="inputs2">
    <table>
    <tr><td>ambient</td><td><input type="range" id="ambient" min="0" max="100" value="20"/></td></tr>
    <tr><td>diffuse</td><td><input type="range" id="diffuse" min="0" max="100" value="50"/></td></tr>
    <tr><td>specular</td><td><input type="range" id="specular" min="0" max="100" value="60"/></td></tr>
    <tr><td>specular tint</td><td><input type="range" id="specular_tint" min="0" max="100" value="33"/></td></tr>
    <tr><td>roughness</td><td><input type="range" id="roughness" min="0" max="100" value="50"/></td></tr>
    <tr><td>metallic</td><td><input type="range" id="metallic" min="0" max="100" value="00"/></td></tr>
    <tr><td>sheen</td><td><input type="range" id="sheen" min="0" max="100" value="50"/></td></tr>
    <tr><td>sheen tint</td><td><input type="range" id="sheen_tint" min="0" max="100" value="50"/></td></tr>
    <tr><td>subsurface</td><td><input type="range" id="subsurface" min="0" max="100" value="5"/></td></tr>
    <tr><td>clear coat</td><td><input type="range" id="clear_coat" min="0" max="100" value="70"/></td></tr>
    <tr><td>clear coat gloss</td><td><input type="range" id="clear_coat_gloss" min="0" max="100" value="70"/></td></tr>
    <tr><td>anisotropic</td><td><input type="range" id="anisotropic" min="0" max="100" value="50"/></td></tr>
</table>
</form></div>  

<div id="gui-bottom"><form name="info">
<dif id="instruction">
<span>Hold the left mouse button pressed and drag to rotate. Release the button to spin.</span><br/>
<span>Press the right mouse button to switch tack and light mode.</span>
</dif>
</form></div> 

<!--div id="ref-link">
  <a href=""> 
  </a>
</div-->

<canvas id="canvas" style="border: none;"></canvas>

<script type="text/javascript">

(function loadscene() {

let gl;

class App {

    constructor() {
        this.canvas = document.getElementById("canvas");
        //gl = canvas.getContext( "webgl2" );
        if ( gl )
            this.ext_uint = true;
        if ( !gl )
            gl = canvas.getContext( "experimental-webgl" );
        if ( !gl )
            gl = canvas.getContext( "webgl" );
        if ( !gl )
            return null;
        
        if ( gl.getExtension('OES_element_index_uint') )
            this.ext_uint = true;
        this.ext_standard_derivatives = gl.getExtension("OES_standard_derivatives");  // dFdx, dFdy
        //if (!ext_standard_derivatives)
        //    console.log('no standard derivatives support (no dFdx, dFdy)');
        //if (!ext_vao)
        //    console.log('no vertex array objects support');

        this.texture = new Texture( "https://raw.githubusercontent.com/Rabbid76/graphics-snippets/master/resource/texture/example_1_texture.bmp" ); 
        this.texture.unit = 1;
        this.texture.bound = false;
        this.displacement = new Texture( "https://raw.githubusercontent.com/Rabbid76/graphics-snippets/master/resource/texture/example_1_heightmap.bmp", [255,255,255,255] ); 
        this.displacement.unit = 2;
        this.displacement.bound = false;

        this.progDraw = new ShProg( 
            [ { source : "draw-shader-vs", stage : gl.VERTEX_SHADER },
              { source : "draw-shader-fs", stage : gl.FRAGMENT_SHADER }
            ] );
        if ( !this.progDraw.progObj )
            return null;
        this.progDraw.inPos = this.progDraw.AttrI("inPos");
        this.progDraw.inTV  = this.progDraw.AttrI("inTV");
        this.progDraw.inBV  = this.progDraw.AttrI("inBV");
        this.progDraw.inNV  = this.progDraw.AttrI("inNV");
        this.progDraw.inCol = this.progDraw.AttrI("inCol");
        this.progDraw.inUV  = this.progDraw.AttrI("inUV");

        this.meshDefs = [ 
            new MeshTetrahedron(),
            new MeshCube(),
            new MeshIcosahedron(),
            new MeshTube(32),
            new MeshCone(32),
            new MeshSphereSlice(16, 32),
            new MeshSphereTessellated(),
            new MeshTorus(32, 32),
            new MeshTrefoilKnot(256, 32),
            new MeshTorusKnot(512, 32, 7, 3),
            new MeshArrow(2, 8, 32)
        ];
        this.i_arrow = this.meshDefs.length-1;

        this.tessellates = [];
        for (let i=0; i < this.meshDefs.length; ++i ) {
            this.tessellates.push( new MeshTessellate( this.meshDefs[i], this.progDraw, this.ext_uint ) );
        }

        //document.getElementById( "mesh" ).value = Math.floor((Math.random() * this.meshDefs.length));
        //let state = Math.random();
        //document.getElementById( "mix_color" ).value = state < 0.33 ? 100 : 0;
        //document.getElementById( "mix_normal" ).checked  = state >= 0.67 ? 1 : 0;

        this.resize(); 
        this.camera = new Camera( [0, 4, 0], [0, 0, 0], [0, 0, 1], 90, this.vp_size, 0.5, 100 );
        this.track = new TrackControl( this.vp_size );
        this.trackLight = new TrackControl( this.vp_size );
        this.trackLight.active = false;
        this.track.attenuation( 1.0, 0.05, 0.0 );

        this.lm = ["ambient", "diffuse", "specular", "specular_tint", "roughness", "metallic", "sheen", "sheen_tint", "subsurface", "clear_coat", "clear_coat_gloss", "anisotropic"];
        this.lmv = new Array(this.lm.length);

        var self = this;
        window.onresize = function() { self.resize(); }
    }    

    resize() {
        //vp_size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
        let view = document.getElementById( "main-view" )
        this.vp_size = view ? [view.clientWidth, window.innerHeight] : [window.innerWidth, window.innerHeight]
        this.canvas.width = this.vp_size[0];
        this.canvas.height = this.vp_size[1];
    }

    draw(deltaMS){
        
        var i_mesh = document.getElementById( "mesh" ).value;
        var fov_val = document.getElementById( "fov" ).value / 100.0;
        var mix_color = document.getElementById( "mix_color" ).value / 100.0;
        var height = 0.2 * document.getElementById( "height" ).value / 100.0;
        var tessellation = document.getElementById( "tessellation" ).value / 100.0;
        var max_dist = Math.max(0.01, tessellation * tessellation).toFixed(4);

        for (let i = 0; i < this.lmv.length; ++ i)
            this.lmv[i] = document.getElementById( this.lm[i] ).value / 100.0;    

        this.tessellates[this.i_arrow].prepare(1.0); // no tessellation
        let changed = this.tessellates[i_mesh].prepare(max_dist);
        if ( changed ) {
            document.getElementById("info").innerHTML = "tessellation distance: " + max_dist + "  vertices: " + this.tessellates[i_mesh].no_of_vertices;
        }
        
        // setup view projection and model
        this.camera.update( this.vp_size, 120.0 - 110 * fov_val );
        this.track.active = !this.trackLight.active;
        this.track.Light  = !this.trackLight.active; 
        this.track.update( this.vp_size );
        this.trackLight.update( this.vp_size );
        
        let projMat       = this.camera.perspective;
        let viewMat       = this.camera.lookAt;
        var modelMat      = Mat44.multiply(this.track.orbit, this.track.autoModelMatrix);
        let lightMat      = Mat44.rotateAxis( Mat44.ident(), 120.0/180.0 * Math.PI, 0); 
        lightMat          = Mat44.rotateAxis( lightMat, 60.0/180.0 * Math.PI, 1); 
        lightMat          = Mat44.multiply(this.trackLight.orbit, lightMat);
        let lightArrowPos = [0.0, 0.0, -1.5, 1];
        let lightPos      = Mat44.transformVec4(lightMat, [lightArrowPos[0], lightArrowPos[1], lightArrowPos[2], 0]);
        lightPos          = Mat44.transformVec4(viewMat, lightPos); 
        var arrowModel    = Mat44.translate(Mat44.ident(), lightArrowPos);
        arrowModel        = Mat44.multiply(lightMat, arrowModel);
            
        //gl.enable(gl.CULL_FACE);
        gl.frontFace(gl.CCW);
        gl.cullFace(gl.BACK);

        gl.viewport( 0, 0, this.vp_size[0], this.vp_size[1] );
        gl.enable( gl.DEPTH_TEST );
        gl.clearColor( 0.0, 0.0, 0.0, 1.0 );
        gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT );

        // bind the texture
        this.texture.bound = this.texture.bound || this.texture.bind( this.texture.unit );
        this.displacement.bound = this.displacement.bound || this.displacement.bind( this.displacement.unit );
        
        // set up draw shader
        this.progDraw.Use();
        this.progDraw.SetM44("u_projectionMat44", projMat);
        this.progDraw.SetM44("u_viewMat44", viewMat);
        this.progDraw.SetM44("u_modelMat44", modelMat);
        this.progDraw.SetI1("u_vertDispMap", this.displacement.unit);
        this.progDraw.SetF1("u_vertDisplScale", height);
        this.progDraw.SetI1("u_texture", this.texture.unit);
        this.progDraw.SetI1("u_displacement_map", this.displacement.unit);
        this.progDraw.SetF2("u_displacement_map_size", this.displacement.size);
        this.progDraw.SetF1("u_displacement_scale", height);
        this.progDraw.SetF1("u_mix_color", mix_color);
        this.progDraw.SetF1("u_mix_normal", 0);

        this.progDraw.SetF4("u_lightPosition", lightPos);
        this.progDraw.SetF4("u_lightColor", [1, 1, 1, 1]);
        for (let i = 0; i < this.lmv.length; ++ i)
            this.progDraw.SetF1("u_" + this.lm[i], this.lmv[i]);
        
        // ambient specular specular_tint roughness metallic sheen sheen_tint subsurface clear_coat clear_coat_gloss anisotropic
        
        // draw scene
        this.tessellates[i_mesh].draw();

        this.progDraw.SetM44("u_modelMat44", arrowModel);
        this.progDraw.SetF1("u_vertDisplScale", 0.0);
        this.progDraw.SetF1("u_displacement_scale", 0.0);
        this.progDraw.SetF1("u_mix_color", 0);
        this.progDraw.SetF1("u_mix_normal", 0);
        this.tessellates[this.i_arrow].draw();
    }
}

class MeshTessellate {
    constructor(meshDef, program, ext_uint) {
        this.meshDef = meshDef;
        this.program = program;
        this.ext_uint = ext_uint;
    }
    prepare(maxDist) {
        maxDist = !maxDist || maxDist < 0.0 ? 0.0 : maxDist;
        if (!this.mesh || this.maxDist == undefined || this.maxDist != maxDist) {
            this.tessellate(maxDist);
            this.no_of_vertices = this.v.length / 3;
            return true;
        }
        return false;
    }

    tessellate(maxDist) {
        this.maxDist = maxDist;

        if (this.meshDef.v) { this.v = this.meshDef.v.slice(0); }
        if (this.meshDef.tv) { this.tv = this.meshDef.tv.slice(0); }
        if (this.meshDef.bv) { this.bv = this.meshDef.bv.slice(0); }
        if (this.meshDef.n) { this.n = this.meshDef.n.slice(0); }
        if (this.meshDef.c) { this.c = this.meshDef.c.slice(0); }
        if (this.meshDef.t) { this.t = this.meshDef.t.slice(0); }
        if (this.meshDef.e) { this.e = this.meshDef.e.slice(0); }

        if ( maxDist > 0.0 )
        {
            let e = this.e;
            if ( !e ) {
                e = new Array(this.v.length/3);
                for ( let i=0; i < this.v.length/3; ++i ) { e[i]=i; }
            }
            this.e = [];

            while ( e.length > 0 ) {
                let en = [];
                for ( let i=0; i < e.length; i += 3 ) {
                    let et = [e[i], e[i+1], e[i+2]];
                    let v0 = this.V(et[0]);
                    let v1 = this.V(et[1]); 
                    let v2 = this.V(et[2]);
                    let l01  = Vec3.len(Vec3.sub(v1,v0));
                    let l12  = Vec3.len(Vec3.sub(v2,v1));
                    let l20  = Vec3.len(Vec3.sub(v0,v2));

                    let next = this.v.length/3;
                    if (l01 > l12 && l01 > l20 && l01 > maxDist) {
                        this.split(et[0], et[1]);
                        let _e = (l01/2 > maxDist || l12 > maxDist || l20 > maxDist) ? en : this.e;
                        _e.push(et[0], next, et[2], next, et[1], et[2]);
                    } else if (l12 > l20 && l12 > maxDist) {
                        this.split(et[1], et[2]);
                        let _e = (l12/2 > maxDist || l20 > maxDist || l01 > maxDist) ? en : this.e;
                        _e.push(et[0], et[1], next, et[0], next, et[2]);
                    } else if (l20 > maxDist) {
                        this.split(et[2], et[0]);
                        let _e = (l20/2 > maxDist || l01 > maxDist || l12 > maxDist) ? en : this.e;
                        _e.push(next, et[1], et[2], et[0], et[1], next);
                    } else {
                        this.e.push(et[0], et[1], et[2]);
                    }
                }
                e = en;
                en = [];
            }
        }

        this.mesh = new VertexBuffer( gl,
            [ { data : this.v,  attrSize : 3, attrLoc : this.program.inPos },
              { data : this.tv, attrSize : 3, attrLoc : this.program.inTV },
              { data : this.bv, attrSize : 3, attrLoc : this.program.inBV },
              { data : this.n,  attrSize : 3, attrLoc : this.program.inNV },
              { data : this.c,  attrSize : 3, attrLoc : this.program.inCol }, 
              { data : this.t,  attrSize : 2, attrLoc : this.program.inUV } 
            ], this.e, gl.TRIANGLES, this.ext_uint );
    }

    V( i ) { return this.v.slice(i*3, i*3+3); } 
    BV( i ) { return this.bv.slice(i*3, i*3+3); } 
    TV( i ) { return this.tv.slice(i*3, i*3+3); } 
    NV( i ) { return this.n.slice(i*3, i*3+3); }
    TC( i ) { return this.t.slice(i*2, i*2+2); } 
    C( i ) { return this.c.slice(i*3, i*3+3); } 
    split(i0, i1) {
        let v = Vec3.mix(this.V(i0), this.V(i1), 0.5);
        this.v.push( v[0], v[1], v[2] );
        let tv = Vec3.mix(this.TV(i0), this.TV(i1), 0.5);
        //let tv = Vec3.normalize(Vec3.add(this.TV(i0), this.TV(i1)));
        this.tv.push( tv[0], tv[1], tv[2] );
        let bv = Vec3.mix(this.BV(i0), this.BV(i1), 0.5);
        //let bv = Vec3.normalize(Vec3.add(this.BV(i0), this.BV(i1)));
        this.bv.push( bv[0], bv[1], bv[2] );
        let n = Vec3.mix(this.NV(i0), this.NV(i1), 0.5);
        //let n = Vec3.normalize(Vec3.add(this.NV(i0), this.NV(i1)));
        this.n.push( n[0], n[1], n[2] );
        let t = Vec2.mix(this.TC(i0), this.TC(i1), 0.5);
        this.t.push( t[0], t[1] );
        let c = Vec3.mix(this.C(i0), this.C(i1), 0.5);
        this.c.push( c[0], c[1], c[2] );
    }
    
    draw() { return this.mesh.Draw(); }
}

class MeshObject {
    constructor() {
        this.v = [], this.n = []; this.t = []; this.c = [];
        this.eps = 0.0001;
    }
    addAttr( v, n, t, c, tv, bv ) {
        let i = this.v.length / 3;
        this.v.push( v[0],v[1],v[2] );
        this.n.push( n[0],n[1],n[2] );
        this.t.push( t[0],t[1] );
        this.c.push( c[0],c[1],c[2] );
        if ( tv && this.tv ) { this.tv.push( tv[0],tv[1],tv[2] ); }
        if ( bv && this.bv ) { this.bv.push( bv[0],bv[1],bv[2] ); }
        return i;
    }
    V( i ) { return this.v.slice(i*3, i*3+3); } 
    BV( i ) { return this.bv.slice(i*3, i*3+3); } 
    TV( i ) { return this.tv.slice(i*3, i*3+3); } 
    NV( i ) { return this.n.slice(i*3, i*3+3); }
    TC( i ) { return this.t.slice(i*2, i*2+2); } 
    C( i ) { return this.c.slice(i*3, i*3+3); } 
    IsBorder(tc) { return Math.abs(tc) < 0.0001 || Math.abs(tc-1) < 0.0001; }
    
    V_EQ(i0, i1) { return Vec3.equal(this.V(i0), this.V(i1), this.eps); }
    NV_EQ(i0, i1) { return Vec3.equal(this.NV(i0), this.NV(i1), this.eps); }

    // find equal vertices and associate equal vertices to equal id 
    equalIds() {
        if (this.vid) return;
        this.vid = new Array(this.v.length/3);
        for ( let i=0; i<this.v.length/3; ++i ) {
            let eq_i = -1;
            for (let j=0; eq_i < 0 && j<i; ++j) { eq_i = this.V_EQ(i,j) ? j : -1; } 
            this.vid[i] = eq_i >= 0 ? eq_i : i;    
        }
        this.eqv = new Array(this.v.length/3);
        for (let i=0; i < this.v.length/3; ++ i ) { this.eqv[i] = []; }
        for (let i=0; i < this.v.length/3; ++ i ) { this.eqv[this.vid[i]].push(i); }
    }

    // create triangle indices if not existing
    indices() {
        if (this.e) return;
        this.e = new Array(this.v.length/3);
        for ( let i=0; i < this.v.length/3; ++i ) {this.e[i]=i; }
    }

    edges() {
        if (this.edg)
            return;
        this.indices();
        this.edg = Array(this.v.length/3);
        for (let i=0; i < this.e.length; ++ i ) { this.edg[i] = []; }
        for (let i=0; i < this.e.length; i += 3 ) {
            let et = [this.e[i], this.e[i+1], this.e[i+2]];
            if ( this.edg[et[0]].indexOf(et[1]) === -1 ) { this.edg[et[0]].push(et[1]); } 
            if ( this.edg[et[0]].indexOf(et[2]) === -1 ) { this.edg[et[0]].push(et[2]); } 
            if ( this.edg[et[1]].indexOf(et[0]) === -1 ) { this.edg[et[1]].push(et[0]); } 
            if ( this.edg[et[1]].indexOf(et[2]) === -1 ) { this.edg[et[1]].push(et[2]); } 
            if ( this.edg[et[2]].indexOf(et[0]) === -1 ) { this.edg[et[2]].push(et[0]); } 
            if ( this.edg[et[2]].indexOf(et[1]) === -1 ) { this.edg[et[2]].push(et[1]); } 
        }
    }
    
    // calculate face tangent and binormal
    createTangentAndBinormal() {
        this.indices();
        this.tv = new Array(this.n.length), this.bv = new Array(this.n.length);
        for ( let i=0; i < this.e.length; i += 3 ) {
            let et = [this.e[i], this.e[i+1], this.e[i+2]];
            let v0 = this.V(et[0]), v1 = this.V(et[1]), v2 = this.V(et[2]);
            let t0 = this.TC(et[0]), t1 = this.TC(et[1]), t2 = this.TC(et[2]); 
            let dp1  = Vec3.sub(v1,v0);
            let dp2  = Vec3.sub(v2,v0);
            let duv1 = Vec2.sub(t1,t0);
            let duv2 = Vec2.sub(t2,t0);
            if ( Vec3.dot(Vec3.cross(dp1, dp2), this.n.slice(et[0]*3, et[0]*3+3)) < 0.0 ) {
                let _dpt = dp1; dp1 = dp2; dp2 = _dpt;
                let _duvt = duv1; duv1 = duv2; duv2 = _duvt;
            }
            // 3*3 matrix which transforms from texture space to scaled texture "wrap" space
            // transforms a normal vector generated from an height map to the texture coordinates
            //let tw_m33 = [duv1[0],duv1[1],0, duv2[0],duv2[1],0, 0,0,1];
            for ( let j=0; j < 3; ++j )  {
                let n = this.n.slice(et[j]*3, et[j]*3+3);
                let dp2perp = Vec3.cross(dp2, n); 
                let dp1perp = Vec3.cross(n, dp1);
                // 3*3 matrix which transforms from texture "wrap" space to model space
                // transforms a wrapped normal vector to the model space  
                //let f_m33 = [dp2perp[0],dp2perp[1],dp2perp[2], dp1perp[0],dp1perp[1],dp1perp[2], n[0],n[1],n[2]];
                // transformation from texture space to triangle face space
                //let m33 = Mat33.multiply(f_m33, tw_m33);
                //this.tv[et[j]*3] = m33[0]; this.tv[et[j]*3+1] = m33[1]; this.tv[et[j]*3+2] = m33[2];
                //this.bv[et[j]*3] = m33[4]; this.bv[et[j]*3+1] = m33[4]; this.bv[et[j]*3+2] = m33[5];
                let tv = Vec3.normalize(Vec3.add(Vec3.mul_s(dp2perp, duv1[0]), Vec3.mul_s(dp1perp, duv2[0])));
                let bv = Vec3.normalize(Vec3.add(Vec3.mul_s(dp2perp, duv1[1]), Vec3.mul_s(dp1perp, duv2[1])));
                this.tv[et[j]*3] = tv[0]; this.tv[et[j]*3+1] = tv[1]; this.tv[et[j]*3+2] = tv[2];
                this.bv[et[j]*3] = bv[0]; this.bv[et[j]*3+1] = bv[1]; this.bv[et[j]*3+2] = bv[2];
            }
        }
        
        // TODO $$$ interpolated tangents
    }

    createWedges() {
        this.indices();
        this.equalIds();
        this.edges();

        // find edges with diverging normal vectors
        let wdg = []
        let wdg_s = []
        for (let i=0; i<this.v.length/3; ++i ) {
            if (this.eqv[i].length<2) continue;
            for ( let i0=0; i0 < this.eqv[i].length-1; ++i0) {
                for ( let i1=1; i1 < this.eqv[i].length; ++i1) {
                    let a = this.eqv[i][i0], b = this.eqv[i][i1]; 
                    if ( this.NV_EQ(a, b) )
                        continue;
                    let ea = this.edg[a], eb = this.edg[b];
                    for ( let j0=0; j0<ea.length; ++j0 ) {
                        for ( let j1=0; j1<eb.length; ++j1 ) {
                            let c = ea[j0], d = eb[j1];
                            if ( c != d && !this.V_EQ(c, d) )
                                continue;
                            let w = [a, b, c, d];
                            let ws = [a, b, c, d];
                            ws.sort();
                            let exists = false;
                            for (let k0=0; exists == false && k0 < wdg_s.length; ++k0) {
                                exists = wdg_s[k0][0] == ws[0] && wdg_s[k0][1] == ws[1] && wdg_s[k0][2] == ws[2] && wdg_s[k0][3] == ws[3];
                            }
                            if ( exists )
                                continue;
                            wdg.push(w);
                            wdg_s.push(ws);
                        }
                    }
                }   
            }
        }
        wdg_s=[];
        
        // create the wedges
        // Note, at the wedges the texture coordinates of vertices with identically 
        // texture coordinates have to be ident, too (be aware of a 0 to 1 coordinate transition).
        // So the vertices w[1] and w[2] possibly have to duplicated, but with the texture coordinates of w[0] respectively w[3]  
        for (let i=0; i < wdg.length; ++i) {
            let w = wdg[i].slice(0);
            let t0 = this.TC(w[0]);
            let t1 = this.TC(w[1]);
            let bd = [ this.IsBorder(t0[0]), this.IsBorder(t0[1]), this.IsBorder(t1[0]),this.IsBorder(t1[1]) ];
            if ( !Vec2.equal(t0, t1, 0.99) || (bd[0] && bd[3]) || (bd[1] && bd[2]) )
                w[1] = this.addAttr( this.V(w[1]), this.NV(w[1]), t0, this.C(w[1]), this.TV(w[1]), this.BV(w[1]) );
            else if ( bd[2] && bd[3] )
                w[0] = this.addAttr( this.V(w[0]), this.NV(w[0]), t1, this.C(w[0]), this.TV(w[0]), this.BV(w[0]) );
            else if ( bd[0] && bd[1] )
                w[1] = this.addAttr( this.V(w[1]), this.NV(w[1]), t0, this.C(w[1]), this.TV(w[1]), this.BV(w[1]) );
            this.e.push(w[1],w[0],w[2]);
            if ( !this.NV_EQ(w[2],w[3]) ) {
                let t2 = this.TC(w[2]);
                let t3 = this.TC(w[3]);
                let bd = [ this.IsBorder(t2[0]), this.IsBorder(t2[1]), this.IsBorder(t3[0]),this.IsBorder(t3[1]) ];
                if ( !Vec2.equal(t2, t3, 0.99) || (bd[0] && bd[3]) || (bd[1] && bd[2]) )
                    w[3] = this.addAttr( this.V(w[3]), this.NV(w[3]), t2, this.C(w[3]), this.TV(w[3]), this.BV(w[3]) );
                else if ( bd[2] && bd[3] )
                    w[2] = this.addAttr( this.V(w[2]), this.NV(w[2]), t3, this.C(w[2]), this.TV(w[2]), this.BV(w[2]) );
                else if ( bd[0] && bd[1] )
                    w[3] = this.addAttr( this.V(w[3]), this.NV(w[3]), t2, this.C(w[3]), this.TV(w[3]), this.BV(w[3]) );
                this.e.push(w[1],w[2],w[3]);
            }
        }
    }
};

class MeshTetrahedron extends MeshObject {
    constructor() {
        super();
        let s_8_9 = Math.sqrt(8/9), s_2_9 = Math.sqrt(2/9), s_2_3 = Math.sqrt(2/3);
        let v = [ 0,0,1, s_8_9,0,-1/3, -s_2_9,s_2_3,-1/3, -s_2_9,-s_2_3,-1/3 ]
        let c = [ 1.0, 0.0, 0.0,  1.0, 1.0, 0.0,  0.0, 0.0, 1.0,  0.0, 1.0, 0.0 ];
        let t = [ 0,0, 0,1, 1,1, 1,0 ];
        let ni = [ 3, 1, 2, 0 ];
        let e = [ 0,1,2, 0,2,3, 0,3,1, 1,3,2 ];  
        this.r = 1;
        for ( let is = 0; is < 4; ++ is ) {
            for ( let ip = 0; ip < 3; ++ ip ) { 
                let i = is*3+ip;
                this.addAttr( [this.r*v[e[i]*3], this.r*v[e[i]*3+1], this.r*v[e[i]*3+2]], 
                [-v[ni[is]*3], -v[ni[is]*3+1], -v[ni[is]*3+2]], [t[e[i]*2], t[e[i]*2+1]], [c[is*3], c[is*3+1], c[is*3+2]] );
            } 
        }
        this.createTangentAndBinormal();
        this.createWedges();
    }
};

class MeshCube extends MeshObject {
    constructor() {
        super();
        let v = [ -1,-1,1,  1,-1,1,  1,1,1, -1,1,1, -1,-1,-1,  1,-1,-1,  1,1,-1, -1,1,-1 ];
        let c = [ 1.0, 0.0, 0.0,   1.0, 0.5, 0.0,    1.0, 0.0, 1.0,   1.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 0.0, 1.0 ];
        let n = [ 0,0,1, 1,0,0, 0,0,-1, -1,0,0, 0,1,0, 0,-1,0 ];
        let t = [ 0,0, 1,0, 1,1, 0,1 ];
        let e = [ 0,1,2,3, 1,5,6,2, 5,4,7,6, 4,0,3,7, 3,2,6,7, 1,0,4,5 ];  
        for ( let i = 0; i < e.length; ++ i ) this.v.push( v[e[i]*3]*0.707, v[e[i]*3+1]*0.707, v[e[i]*3+2]*0.707 );
        for ( let is = 0; is < 6; ++ is ) {
            for ( let ip = 0; ip < 4; ++ ip ) { 
                this.n.push( n[is*3], n[is*3+1], n[is*3+2] ); 
                this.c.push( c[is*3], c[is*3+1], c[is*3+2] ); 
                this.t.push( t[ip*2], t[ip*2+1] ); 
            } 
        }
        this.e = [];
        for ( let i = 0; i < e.length; i += 4 ) { this.e.push( i, i+1, i+2, i, i+2, i+3 ); }
        this.createTangentAndBinormal();   
        this.createWedges();
    }
};

class MeshIcosahedron extends MeshObject {
    constructor() {
        super();
        let v = [ 0,0,1, 0.894,0,0.447, 0.276,0.851,0.447, -0.724,0.526,0.447, -0.724,-0.526,0.447, 0.276,-0.851, 0.447,
                  0.724,0.526,-0.447, -0.276,0.851,-0.447, -0.894,0,-0.447, -0.276,-0.851,-0.447, 0.724,-0.526,-0.447, 0,0,-1 ];
        let c = [ 1.0,0.0,0.0, 0.0,1.0,0.0, 0.0,0.0,1.0, 1.0,1.0,0.0, 1.0,0.0,1.0, 1.0,0.5,0.0, 0.0,1.0,1.0 ];
        let e = [ 1,2,0, 2,3,0, 3,4,0, 4,5,0, 5,1,0, 7,6,11, 8,7,11, 9,8,11, 10,9,11, 6,10,11,
                  1,6,2, 2,7,3, 3,8,4, 4,9,5, 5,10,1, 7,2,6, 8,3,7, 9,4,8, 10,5,9,  6,1,10 ];
        let t = [0,0, 1,0, 1,1, 0,1];
        for ( let i = 0; i < e.length; ++ i ) {
           this.v.push( v[e[i]*3], v[e[i]*3+1], v[e[i]*3+2] );
           let ti = i % 3, fi = i / 3;
           let sift = ( Math.floor(fi) >= 10 || ti == 0) ? 0 : 2;
           this.t.push( t[ti*2+sift], t[ti*2+1+sift] );
        }
        for ( let fi = 0; fi < this.v.length/9; ++ fi ) {
            let n = [0, 0, 0];
            for ( let ti = 0; ti < 3; ++ ti ) {
                n[0] += this.v[(fi*3+ti)*3];
                n[1] += this.v[(fi*3+ti)*3+1];
                n[2] += this.v[(fi*3+ti)*3+2];
            }
            n = Vec3.normalize(n);
            let ci = fi % (c.length/3);
            for ( let ti = 0; ti < 3; ++ ti ) {
                this.n.push( n[0], n[1], n[2] );
                this.c.push( c[ci*3], c[ci*3+1], c[ci*3+2] );
            }
        }
        this.createTangentAndBinormal();
        this.createWedges();
    }
};

class MeshTube extends MeshObject {
    constructor(nc, nl, r, h, c) {
        super();
        nl = nl || 1;
        nc = nc || 32;
        h = h || 2;
        r = r || h/Math.PI;
        c = c || [0.2, 0.4, 0.8];
        // bottom cap
        this.e = [];
        this.addAttr( [0, 0, h/2], [0, 0, 1], [0.5, 0.5], c );
        for ( let i = 0; i <= nc; i ++ ) {
            let a = 2 * Math.PI * i / nc;
            let x = Math.cos( a ), y = Math.sin( a );
            let uvs = 1/Math.abs(Math.abs(x)>Math.abs(y) ? x : y);
            let u = uvs*x*0.5+0.5;
            let v = uvs*y*0.5+0.5;
            this.addAttr( [r*x, r*y, h/2], [0, 0, 1], [u, v], [c[0], c[1], c[2]] ); 
            if ( i < nc)
                this.e.push( 0, i+1, i+2 );
        }
        // discs
        let st = this.v.length/3;
        let nc_2 = nc / 2;
        let ratio = 2*h / (r*2*Math.PI);
        for ( let tbI = 0; tbI <= nl; tbI ++ )
        {
            let v = ( 1.0 - tbI / nl ) * ratio;
            let z =   h/2 - h * tbI / nl;
            // create 2 hemispheres, so would be possible to wrap 1 texture at both hemispheres 
            for ( let hi = 0; hi < 2; ++ hi ) {
                for ( let i = 0; i <= nc_2; i ++ ) {
                    let u = i / nc_2;
                    let a = Math.PI * u + hi * Math.PI;
                    let x = Math.cos( a ), y = Math.sin( a );
                    this.addAttr( [r*x, r*y, z], [x, y, 0], [u /*hi*0.5+u*0.5*/, v], c );
                }
            }
        }
        let cs_2 = nc_2 + 1;
        for ( let tbI = 0; tbI < nl; tbI ++ ) {
            for ( let hi = 0; hi < 2; ++ hi ) {
                let rsi = st +tbI*2*cs_2 + hi*cs_2;
                let nrsi = rsi + 2*cs_2;
                for ( let i = 0; i < nc_2; i ++ ) { this.e.push( rsi+i, nrsi+i, nrsi+i+1, rsi+i, nrsi+i+1, rsi+i+1 ); }
            }
        }
        // bottom cap
        st = this.v.length/3;
        for ( let i = 0; i <= nc; i ++ ) {
            let a = 2 * Math.PI * i / nc;
            let x = Math.cos( a ), y = Math.sin( a );
            let uvs = 1/Math.abs(Math.abs(x)>Math.abs(y) ? x : y);
            let u = uvs*x*0.5+0.5;
            let v = uvs*y*0.5+0.5;
            this.addAttr( [r*x, r*y, -h/2], [0, 0, -1], [u, v], c );
            if ( i < nc)
                this.e.push( st+nc+1, st+i+1, st+i );
        } 
        this.addAttr( [0, 0, -h/2], [0, 0, -1], [0.5, 0.5], c );
        this.createTangentAndBinormal();
        this.createWedges();
    }
};

class MeshCone extends MeshObject {
    constructor(nc, nl, r, h, c) {
        super();
        nl = nl || 4;
        nc = nc || 32;
        h = h || 2;
        r = r || h/Math.PI*1.5;
        c = c || [0.2, 0.7, 0.3];
        this.e = [];
        // discs
        let st = 0;
        let nc_2 = nc / 2;
        let ratio = 2*h / (r*2*Math.PI);
        for ( let tbI = 0; tbI <= nl; tbI ++ )
        {
            var v = ( 1.0 - tbI / nl ) * ratio * 3 / 2;
            var z = h*2/3 - h * tbI / nl;
            var d = tbI / nl;
            // create 2 hemispheres, so would be possible to wrap 1 texture at both hemispheres 
            for ( let hi = 0; hi < 2; ++ hi ) {
                for ( let i = 0; i <= nc_2; i ++ ) {
                    var u = i / nc_2;
                    var a = Math.PI * u + hi * Math.PI;
                    var x = Math.cos( a ), y = Math.sin( a );
                    var n = Vec3.normalize( [x, y, r/h] );
                    this.addAttr( [d * r*x, d* r*y, z], n, [u /*hi*0.5+u*0.5*/, v], c );
                }
            }
        }
        let cs_2 = nc_2 + 1;
        for ( let tbI = 0; tbI < nl; tbI ++ ) {
            for ( let hi = 0; hi < 2; ++ hi ) {
                let rsi = st +tbI*2*cs_2 + hi*cs_2;
                let nrsi = rsi + 2*cs_2;
                for ( let i = 0; i < nc_2; i ++ ) { 
                    this.e.push( rsi+i, nrsi+i, nrsi+i+1 );
                    if ( tbI > 0 ) 
                        this.e.push( rsi+i, nrsi+i+1, rsi+i+1 ); 
                }
            }
        }
        // bottom cap
        st = this.v.length/3;
        for ( let i = 0; i <= nc; i ++ ) {
            var a = 2 * Math.PI * i / nc;
            var x = Math.cos( a ), y = Math.sin( a );
            let uvs = 1/Math.abs(Math.abs(x)>Math.abs(y) ? x : y);
            let u_ = uvs*x*0.5+0.5;
            let v_ = uvs*y*0.5+0.5;
            this.addAttr( [r*x, r*y, -h/3], [0, 0, -1], [u_, v_], c );
            if ( i < nc)
                this.e.push( st+nc+1, st+i+1, st+i );
        } 
        this.addAttr( [0, 0, -h/3], [0, 0, -1], [0.5, 0.5], c );
        this.createTangentAndBinormal();
        this.createWedges();
    }
};

class MeshSphereSlice extends MeshObject {
    constructor(nl, nc, r, c) {
        super();
        nl = nl || 16;
        nc = nc || 32;
        r = r || 1;
        c = c || [0.7, 0.7, 0.2];
        this.v = []; this.n = []; this.c = [];  this.t = [];
        let nc_2 = nc / 2;
        for ( let tbI = 0; tbI <= nl; tbI ++ )
        {
            var v = ( 1.0 - tbI / nl );
            var z = Math.sin( ( 1.0 - 2.0 * tbI / nl ) * Math.PI/2.0 );
            var cU = Math.sqrt( 1.0 - z*z );
            // create 2 hemispheres, so would be possible to wrap 1 texture at both hemispheres 
            for ( let hi = 0; hi < 2; ++ hi ) {
                for ( let i = 0; i <= nc_2; i ++ ) {
                    var u = i / nc_2;
                    var a = Math.PI * u + hi * Math.PI;
                    var x = Math.cos( a ) * cU, y = Math.sin( a ) * cU;
                    this.addAttr( [r*x, r*y, r*z], [x, y, z], [hi*0.5+u*0.5, v], c );
                }
            }
        }
        // bottom cap
        this.e = [];
        var cs_2 = nc_2 + 1;
        var cs = cs_2 * 2;
        for ( let i = 0; i < nc_2; i ++ ) { this.e.push( cs + i, cs + i + 1, i ); }
        for ( let i = nc_2+1; i < 2*nc_2+1; i ++ ) { this.e.push( cs+i, cs+i+1, i ); }
        // discs
        for ( let tbI = 1; tbI < nl - 1; tbI ++ ) {
            for ( let hi = 0; hi < 2; ++ hi ) {
                var rsi = tbI*cs + hi*cs_2;
                var nrsi = (tbI+1)*cs + hi*cs_2;
                for ( var i = 0; i < nc_2; i ++ ) { this.e.push( rsi+i, nrsi+i, nrsi+i+1, rsi+i, nrsi+i+1, rsi+i+1 ); }
            }
        }
        // top cap
        var si = (nl-1) * cs;
        for ( let i = 0; i < nc_2; i ++ ) { this.e.push( si + i + 1, si + i, si + i + cs ); }
        for ( let i = nc_2+1; i < 2*nc_2+1; i ++ ) { this.e.push( si+i+1, si+i, si+i+cs ); }
        this.createTangentAndBinormal();
        this.createWedges();
    }
};

class MeshSphereTessellated extends MeshTetrahedron {
    constructor( tc ) {
        super();
        tc = tc || 5;
        this.n = [];
        for ( let i = 0; i < this.v.length; ++ i )
            this.n.push(this.v[i]/this.r);
        this.e = [0,1,2,3,4,5,6,7,8,9,10,11];
        for ( let i = 0; i < tc; ++ i )
           this.SplitTriangle();
        this.createTangentAndBinormal();
    }
    SplitLine( i0, i1 ) {
        let v = Vec3.mul_s(Vec3.normalize([ (this.v[i0*3]+this.v[i1*3])/2, (this.v[i0*3+1]+this.v[i1*3+1])/2, (this.v[i0*3+2]+this.v[i1*3+2])/2 ]),this.r);
        let n = Vec3.normalize([ (this.n[i0*3]+this.n[i1*3])/2, (this.n[i0*3+1]+this.n[i1*3+1])/2, (this.n[i0*3+2]+this.n[i1*3+2])/2 ]);
        let t = [ (this.t[i0*2]+this.t[i1*2])/2, (this.t[i0*2+1]+this.t[i1*2+1])/2 ];
        let c = [ (this.c[i0*3]+this.c[i1*3])/2, (this.c[i0*3+1]+this.c[i1*3+1])/2, (this.c[i0*3+2]+this.c[i1*3+2])/2 ];
        this.addAttr( v, n, t, c );
    }
    SplitTriangle() {
        var faces = this.e;
        var noOfIndices = faces.length;   
        this.e = [];
        for( let faceStart = 0; faceStart < noOfIndices; faceStart = faceStart + 3 ) {
            let f = [ faces[faceStart], faces[faceStart+1], faces[faceStart+2] ];
            let next = this.v.length/3;
            let nf = [ next, next+1, next+2 ];
            this.SplitLine( f[0], f[1] );
            this.SplitLine( f[1], f[2] );
            this.SplitLine( f[2], f[0] );
            this.e.push( f[0], nf[0], nf[2] );
            this.e.push( f[1], nf[1], nf[0] );
            this.e.push( f[2], nf[2], nf[1] );
            this.e.push( nf[0], nf[1], nf[2] );
        }
    }
};

class MeshTorus extends MeshObject {
    constructor(nl, nc, r, rt, c) {
        super();
        nl = nl || 32;
        nc = nc || 32;
        c = c || [0.9, 0.5, 0.1];
        r = r || 0.7;
        rt = rt || 0.3;
        this.v = []; this.n = []; this.c = [];  this.t = []; this.e = [];
        for ( let i_l = 0; i_l <= nl; ++ i_l ) {
            let tcpt = [Math.cos(2 * Math.PI * i_l / nl), Math.sin(2 * Math.PI * i_l / nl) ];
            for ( let i_c = 0; i_c <= nc; ++ i_c ) {
                let tX = Math.cos(2 * Math.PI * i_c / nc), tY = Math.sin(2 * Math.PI * i_c / nc);
                let p  = [ tcpt[0] * (r + tX*rt), tcpt[1] * (r + tX*rt), tY*rt ];
                let n  = [ p[0] - tcpt[0]*r, p[1] - tcpt[1]*r, p[2] ];
                this.addAttr( p, n, [3*i_l/nl, i_c/nc], c );
                let i_l0 = i_l * (nc+1), i_l1 = (i_l+1) * (nc+1); 
                if (i_c < nc && i_l < nl)
                    this.e.push( i_l0+i_c, i_l1+i_c, i_l0+(i_c+1), i_l0+(i_c+1), i_l1+i_c, i_l1+(i_c+1) );
            }
        }
        this.createTangentAndBinormal();
    }
};

class MeshTrefoilKnot extends MeshObject {
    constructor(slices, stacks, ra, rb, rc, rd, c) {
        super();
        slices = slices || 256;
        stacks = stacks || 32;
        this.ra = ra || 0.6;
        this.rb = ra || 0.2;
        this.rc = ra || 0.4;
        this.rd = rd || 0.175;
        c = c || [0.9, 0.5, 0.1];
        this.v = []; this.n = []; this.c = []; this.e = []; this.t = [];
        let ds = 1.0 / slices, dt = 1.0 / stacks;
        let E = 0.01;
        for ( let s = 0; s < 1 + ds / 2; s += ds) {
            for ( let t = 0; t < 1 + dt / 2; t += dt ) {
                let p = this.compute( s, t );
                let u = this.compute( s + E, t );
                u = Vec3.sub( u, p );
                let v = this.compute( s, t + E );
                v = Vec3.sub( v, p );
                let n = Vec3.normalize(Vec3.cross( u, v ));
                this.addAttr( p, n, [s * 18.0, t], c );
            }
        }
        this.e = [];
        let vertexCount = this.v.length/3;
        let n = 0;
        for ( let i = 0; i < slices; ++ i ) {
            for ( let j = 0; j < stacks; ++ j ) { 
                this.e.push( n + j, (n + j + stacks + 1) % vertexCount, n + j + 1 );
                this.e.push( (n + j + stacks + 1) % vertexCount, (n + j + 1 + stacks + 1) % vertexCount, (n + j + 1) % vertexCount );
            }
            n += stacks+1;
        }
        this.createTangentAndBinormal();
    }

    compute( s, t ) {
        let TwoPi = Math.PI * 2;
        let a = this.ra;
        let b = this.rb;
        let c = this.rc;
        let d = this.rd;
        let u = ( 1 - s ) * 2 * TwoPi;
        let v = t * TwoPi;
        let r = a + b * Math.cos( 1.5 * u );
        let x = r * Math.cos( u );
        let y = r * Math.sin( u );
        let z = c * Math.sin( 1.5 * u );
        let dv = [
            -1.5 * b * Math.sin( 1.5 * u ) * Math.cos( u ) - ( a + b * Math.cos( 1.5 * u ) ) * Math.sin( u ),
            -1.5 * b * Math.sin( 1.5 * u ) * Math.sin( u ) + ( a + b * Math.cos( 1.5 * u ) ) * Math.cos( u ),
            1.5 * c * Math.cos( 1.5 * u )
        ];
        let q = Vec3.normalize( dv );
        let qvn = Vec3.normalize( [ q[1], -q[0], 0 ] );
        let ww = Vec3.cross( q, qvn );
        let range = [
            x + d * ( qvn[0] * Math.cos( v ) + ww[0] * Math.sin( v ) ),
            y + d * ( qvn[1] * Math.cos( v ) + ww[1] * Math.sin( v ) ),
            z + d * ww[2] * Math.sin( v )
        ]    
        return range;
    }
};

class MeshTorusKnot extends MeshObject {
    constructor(tess_u, tess_v, p_, q_, ra, rb, rc, c) {
        super();
        tess_u = tess_u || 512;
        tess_v = tess_v || 32;
        p_ = p_ || 7;
        q_ = q_ || 3; 
        ra = ra || 0.7;
        rb = rb || 0.3;
        rc = rc || 0.1;
        c = c || [0.9, 0.5, 0.1];
        this.v = []; this.n = []; this.c = []; this.e = []; this.t = [];
        
        for (let u = 0; u <= tess_u; ++ u ) {
            let phi0 = 2 * Math.PI * u / tess_u;
            let pt_tk0 = this.sample(phi0, p_, q_, ra, rb)
            let phi1 = 2 * Math.PI * (u + 1) / tess_u
            let pt_tk1 = this.sample(phi1, p_, q_, ra, rb)
            let pt_c = this.sample(phi0, p_, q_, ra, 0)
            
            let T = Vec3.sub(pt_tk1, pt_tk0);
            let B = Vec3.normalize(Vec3.sub(pt_tk0, pt_c));
            let N = Vec3.normalize(Vec3.cross(T, B))
            T = Vec3.normalize(Vec3.cross(B, N))
            for (let v = 0; v <= tess_v; ++ v ) {
                let theta = 2 * Math.PI * (v / tess_v);
                let px = Math.sin(theta) * rc; 
                let py = Math.cos(theta) * rc;
                let n = Vec3.add(Vec3.mul_s(N, px), Vec3.mul_s(B, py));
                let p = Vec3.add(n, pt_tk0);
                n = Vec3.normalize(n);
                this.addAttr( p, n, [3.0 * p_ * q_ * u/tess_u, v/tess_v], c );
            }
        }
        
        this.e = [];
        for (let i_strip = 0; i_strip < tess_u; ++ i_strip ) {
            for (let i_v = 0; i_v < tess_v; ++ i_v ) {
                let ei = [i_strip*(tess_v+1)+i_v, i_strip*(tess_v+1)+i_v+1, (i_strip+1)*(tess_v+1)+i_v+1, (i_strip+1)*(tess_v+1)+i_v];
                this.e.push(ei[0], ei[1], ei[2]);
                this.e.push(ei[0], ei[2], ei[3]);
            }
        }
        
        this.createTangentAndBinormal();
    }

    sample(phi, p, q, r1, r2) {
        var vertex = [
            (r1 + r2 * Math.cos(p * phi)) * Math.cos(q * phi),
            (r1 + r2 * Math.cos(p * phi)) * Math.sin(q * phi), 
            r2 * -Math.sin(p * phi)
        ]; 
        return vertex;
    }
};

class MeshArrow extends MeshObject {
    constructor(nlp, nls, nc, ls, lp, rs, rp, c) {
        super();
        nlp = nlp || 2;
        nls = nls || 8;
        nc = nc || 32;
        lp = lp || 0.25;
        ls = ls || 0.75;
        rp = rp || lp/Math.PI*1.5;
        rs = rs || lp/Math.PI*0.75;
        c = c || [0.7, 0.2, 0.3];
        this.v = []; this.n = []; this.c = []; this.t = []; this.e = [];
        // cone discs
        let st = 0;
        let nc_2 = nc / 2;
        let p_ratio = 2*lp / (rp*2*Math.PI);
        for ( let tbI = 0; tbI <= nlp; tbI ++ )
        {
            var v = ( 1.0 - tbI / nlp ) * p_ratio * 3 / 2;
            var z = -lp * tbI / nlp;
            var d = tbI / nlp;
            // create 2 hemispheres, so would be possible to wrap 1 texture at both hemispheres 
            for ( let hi = 0; hi < 2; ++ hi ) {
                for ( let i = 0; i <= nc_2; i ++ ) {
                    var u = i / nc_2;
                    var a = Math.PI * u + hi * Math.PI;
                    var x = Math.cos( a );
                    var y = Math.sin( a );
                    var n = Vec3.normalize( [x, y, rp/lp] );
                    this.addAttr( [d * rp*x, d* rp*y, z], n, [u /*hi*0.5+u*0.5*/, v], c );
                }
            }
        }
        let cs_2 = nc_2 + 1;
        for ( let tbI = 0; tbI < nlp; tbI ++ ) {
            for ( let hi = 0; hi < 2; ++ hi ) {
                let rpi = st +tbI*2*cs_2 + hi*cs_2;
                let nrpi = rpi + 2*cs_2;
                for ( let i = 0; i < nc_2; i ++ ) { 
                    this.e.push( rpi+i, nrpi+i, nrpi+i+1 );
                    if ( tbI > 0 ) 
                        this.e.push( rpi+i, nrpi+i+1, rpi+i+1 ); 
                }
            }
        }
        // cone bottom cap
        st = this.v.length/3;
        for ( let i = 0; i <= nc; i ++ ) {
            var a = 2 * Math.PI * i / nc;
            var x = Math.cos( a );
            var y = Math.sin( a );
            let uvs = 1/Math.abs(Math.abs(x)>Math.abs(y) ? x : y);
            let u_ = uvs*x*0.5+0.5;
            let v_ = uvs*y*0.5+0.5;
            this.addAttr( [rp*x, rp*y, -lp], [0, 0, -1], [u_, v_], c );
            if ( i < nc)
                this.e.push( st+nc+1, st+i+1, st+i );
        } 
        this.addAttr( [0, 0, -lp], [0, 0, -1], [0.5, 0.5], c );
        // shaft discs
        st = this.v.length/3;
        let s_ratio = 2*ls / (rs*2*Math.PI);
        for ( let tbI = 0; tbI <= nls; tbI ++ )
        {
            var v = ( 1.0 - tbI / nls ) * s_ratio;
            var z =   -lp - ls * tbI / nls;
            // create 2 hemispheres, so would be possible to wrap 1 texture at both hemispheres 
            for ( let hi = 0; hi < 2; ++ hi ) {
                for ( let i = 0; i <= nc_2; i ++ ) {
                    var u = i / nc_2;
                    var a = Math.PI * u + hi * Math.PI;
                    var x = Math.cos( a );
                    var y = Math.sin( a );
                    this.addAttr( [rs*x, rs*y, z], [x, y, 0], [u /*hi*0.5+u*0.5*/, v], c );
                }
            }
        }
        for ( let tbI = 0; tbI < nls; tbI ++ ) {
            for ( let hi = 0; hi < 2; ++ hi ) {
                let rsi = st +tbI*2*cs_2 + hi*cs_2;
                let nrsi = rsi + 2*cs_2;
                for ( let i = 0; i < nc_2; i ++ ) { this.e.push( rsi+i, nrsi+i, nrsi+i+1, rsi+i, nrsi+i+1, rsi+i+1 ); }
            }
        }
        // bottom cap
        st = this.v.length/3;
        for ( let i = 0; i <= nc; i ++ ) {
            var a = 2 * Math.PI * i / nc;
            var x = Math.cos( a );
            var y = Math.sin( a );
            let uvs = 1/Math.abs(Math.abs(x)>Math.abs(y) ? x : y);
            let u_ = uvs*x*0.5+0.5;
            let v_ = uvs*y*0.5+0.5;
            this.addAttr( [rs*x, rs*y, -lp-ls], [0, 0, -1], [u_, v_], c );
            if ( i < nc)
                this.e.push( st+nc+1, st+i+1, st+i );
        } 
        this.addAttr( [0, 0, -lp-ls], [0, 0, -1], [0.5, 0.5], c );
        this.createTangentAndBinormal();
        this.createWedges();
    }
};

class ShProg {
    constructor(shaderList) {
        let shaderObjs = [];
        for (let i_sh = 0; i_sh < shaderList.length; ++i_sh) {
            let shderObj = this.Compile(shaderList[i_sh].source, shaderList[i_sh].stage);
            if (shderObj) shaderObjs.push(shderObj);
        }
        this.progObj = this.Link(shaderObjs)
        if (this.progObj) {
            this.attrInx = {};
            var noOfAttributes = gl.getProgramParameter(this.progObj, gl.ACTIVE_ATTRIBUTES);
            for (var i_n = 0; i_n < noOfAttributes; ++i_n) {
                var name = gl.getActiveAttrib(this.progObj, i_n).name;
                this.attrInx[name] = gl.getAttribLocation(this.progObj, name);
            }
            this.uniLoc = {};
            var noOfUniforms = gl.getProgramParameter(this.progObj, gl.ACTIVE_UNIFORMS);
            for (var i_n = 0; i_n < noOfUniforms; ++i_n) {
                var name = gl.getActiveUniform(this.progObj, i_n).name;
                this.uniLoc[name] = gl.getUniformLocation(this.progObj, name);
            }
        }
    }

    AttrI(name) { return this.attrInx[name]; }
    UniformL(name) { return this.uniLoc[name]; }
    Use() { gl.useProgram(this.progObj); }
    SetI1(name, val) { if (this.uniLoc[name]) gl.uniform1i(this.uniLoc[name], val); }
    SetF1(name, val) { if (this.uniLoc[name]) gl.uniform1f(this.uniLoc[name], val); }
    SetF2(name, arr) { if (this.uniLoc[name]) gl.uniform2fv(this.uniLoc[name], arr); }
    SetF3(name, arr) { if (this.uniLoc[name]) gl.uniform3fv(this.uniLoc[name], arr); }
    SetF4(name, arr) { if (this.uniLoc[name]) gl.uniform4fv(this.uniLoc[name], arr); }
    SetM33(name, mat) { if (this.uniLoc[name]) gl.uniformMatrix3fv(this.uniLoc[name], false, mat); }
    SetM44(name, mat) { if (this.uniLoc[name]) gl.uniformMatrix4fv(this.uniLoc[name], false, mat); }
    
    Compile(source, shaderStage) {
        let shaderScript = document.getElementById(source);
        if (shaderScript)
            source = shaderScript.text;
        let shaderObj = gl.createShader(shaderStage);
        gl.shaderSource(shaderObj, source);
        gl.compileShader(shaderObj);
        let status = gl.getShaderParameter(shaderObj, gl.COMPILE_STATUS);
        if (!status) alert(gl.getShaderInfoLog(shaderObj));
        return status ? shaderObj : null;
    }
    
    Link(shaderObjs) {
        let prog = gl.createProgram();
        for (let i_sh = 0; i_sh < shaderObjs.length; ++i_sh)
            gl.attachShader(prog, shaderObjs[i_sh]);
        gl.linkProgram(prog);
        status = gl.getProgramParameter(prog, gl.LINK_STATUS);
        if ( !status ) alert(gl.getProgramInfoLog(prog));
        return status ? prog : null;
    } 
}

class VertexBuffer {
    constructor(gl, attribs, indices, type, ext_unit) {
        this.gl = gl;
        this.buf = [];
        this.attr = [];
        this.ext_uint = ext_unit;
        if ( indices )
            this.inxLen = indices.length;
        this.primitive_type = type ? type : gl.TRIANGLES;
        if ( this.gl.createVertexArray )
        {
            this.vao = this.gl.createVertexArray();
            this.gl.bindVertexArray(this.vao);
            this.Prepare = function() { this.gl.bindVertexArray(this.vao); };
            this.Release = function() { this.gl.bindVertexArray(null); };
        }
        else if ( this.ext_vao = gl.getExtension("OES_vertex_array_object") ) {
            this.vao = this.ext_vao .createVertexArrayOES();
            this.ext_vao.bindVertexArrayOES(this.vao);
            this.Prepare = function() { this.ext_vao.bindVertexArrayOES(this.vao); };
            this.Release = function() { this.ext_vao.bindVertexArrayOES(null); };
        }
        else {
            this.Prepare = function() { 
                this.DefineVBOs(); 
                if ( this.inxLen > 0 ) this.gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.inx); 
            };
            this.Release = function() { 
                if ( this.inxLen > 0 )
                    this.gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
                for (let i=0; i<this.buf.length; ++i)
                    this.gl.disableVertexAttribArray(this.attr[i].loc);
            };
        }

        for (let i=0; i<attribs.length; ++i) {
            if ( attribs[i].data != undefined && attribs[i].attrLoc != undefined && attribs[i].attrLoc >= 0 ) {
                this.buf.push(this.gl.createBuffer());
                this.attr.push({ size : attribs[i].attrSize, loc : attribs[i].attrLoc, no_of: attribs[i].data.length/attribs[i].attrSize });
                this.gl.bindBuffer(gl.ARRAY_BUFFER, this.buf[i]);
                this.gl.bufferData(gl.ARRAY_BUFFER, new Float32Array( attribs[i].data ), gl.STATIC_DRAW);
            }
        }
        this.gl.bindBuffer(gl.ARRAY_BUFFER, null);
        if ( this.inxLen > 0 ) {
            this.inx = this. gl.createBuffer();
            this.gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.inx);
            let indArray;
            if ( indices.length > 65536 && this.ext_uint) {
                this.index_type = gl.UNSIGNED_INT;
                indArray = new Uint32Array( indices );
            } else if ( indices.length > 256 ) {
                this.index_type = gl.UNSIGNED_SHORT;
                indArray = new Uint16Array( indices );
            } else {
                this.index_type = gl.UNSIGNED_BYTE;
                indArray = new Uint8Array( indices );
            }
            this.gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indArray, gl.STATIC_DRAW);
            if ( !this.vao )
                this.gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
        }
        if ( this.ext_vao || this.vao ) {
            this.DefineVBOs();
            this.Release()
        }
    }

    Draw() {
        this.Prepare();
        if (this.inxLen > 0)
            this.gl.drawElements(this.primitive_type, this.inxLen, this.index_type, 0);
        else
            this.gl.drawArrays(this.primitive_type, 0, this.attr[0].no_of);
        this.Release();
    }

    DefineVBOs() {
        for (let i=0; i<this.buf.length; ++i) {
            this.gl.bindBuffer(gl.ARRAY_BUFFER, this.buf[i]);
            this.gl.vertexAttribPointer(this.attr[i].loc, this.attr[i].size, gl.FLOAT, false, 0, 0);
            this.gl.enableVertexAttribArray(this.attr[i].loc);
        }
        this.gl.bindBuffer(gl.ARRAY_BUFFER, null);
    }
}

class Texture {
    constructor( name, dflt ) {
        let texture = this;
        this.dflt = dflt || [128,128,128,255]
        let image = { "cx": this.dflt.w || 1, "cy": this.dflt.h || 1, "plane": this.dflt.p || this.dflt };
        this.size = [image.cx, image.cy];
        this.dummyObj = Texture.createTexture2D( image, true )
        this.image = new Image(64,64);
        this.image.setAttribute('crossorigin', 'anonymous');
        this.image.onload = function () {
            let cx = 1 << 31 - Math.clz32(texture.image.width);
            if ( cx < texture.image.naturalWidth ) cx *= 2;
            let cy = 1 << 31 - Math.clz32(texture.image.height);
            if ( cy < texture.image.naturalHeight ) cy *= 2;
            var canvas = document.createElement( 'canvas' );
            canvas.width  = cx;
            canvas.height = cy;
            var context = canvas.getContext( '2d' );
            context.drawImage( texture.image, 0, 0, canvas.width, canvas.height );
            texture.textureObj = Texture.createTexture2D( canvas, true );
            texture.size = [cx, cy];
        }
        this.image.src = name;
    }
    static createTexture2D( image, flipY ) {
        let t = gl.createTexture();
        gl.activeTexture( gl.TEXTURE0 );
        gl.bindTexture( gl.TEXTURE_2D, t );
        gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, flipY != undefined && flipY == true );
        if ( image.cx && image.cy && image.plane )
            gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, image.cx, image.cy, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(image.plane) );
        else
            gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT );
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT );
        gl.bindTexture( gl.TEXTURE_2D, null );
        return t;
    }
    bind( texUnit ) {
        gl.activeTexture( gl.TEXTURE0 + texUnit );
        if ( this.textureObj ) { 
            gl.bindTexture( gl.TEXTURE_2D, this.textureObj );
            return true;
        }
        gl.bindTexture( gl.TEXTURE_2D, this.dummyObj );
        return false;
    }
    bindDflt( texUnit ) {
        gl.activeTexture( gl.TEXTURE0 + texUnit );
        gl.bindTexture( gl.TEXTURE_2D, this.dummyObj );
        return false;
    }
};

class Camera {
    constructor( pos, target, up, fov_y, vp, near, far ) {
        this.pos = pos;
        this.target = target;
        this.up = up;
        this.fov_y = fov_y;
        this.vp = vp;
        this.near = near;
        this.far = far;
    }

    get ortho() {
        let fn = this.far + this.near;
        let f_n = this.far - this.near;
        let w = this.vp[0];
        let h = this.vp[1];
        let a = w/h;
        return [ 2/a, 0, 0, 0, 0, 2, 0, 0, 0, 0, -2/f_n, 0, 0, 0, -fn/f_n, 1 ];
    }  

    get perspective() {
        let n = this.near;
        let f = this.far;
        let fn = f + n;
        let f_n = f - n;
        let r = this.vp[0] / this.vp[1];
        let t = 1 / Math.tan( Math.PI * this.fov_y / 360 );
        return [ t/r, 0, 0, 0, 0, t, 0, 0, 0, 0, -fn/f_n, -1, 0, 0, -2*f*n/f_n, 0 ];
    } 

    get lookAt() {
        let mz = Vec3.normalize( [ this.pos[0]-this.target[0], this.pos[1]-this.target[1], this.pos[2]-this.target[2] ] );
        let mx = Vec3.normalize( Vec3.cross( this.up, mz ) );
        let my = Vec3.normalize( Vec3.cross( mz, mx ) );
        let tx = Vec3.dot( mx, this.pos );
        let ty = Vec3.dot( my, this.pos );
        let tz = Vec3.dot( [-mz[0], -mz[1], -mz[2]], this.pos ); 
        return [mx[0], my[0], mz[0], 0, mx[1], my[1], mz[1], 0, mx[2], my[2], mz[2], 0, tx, ty, tz, 1]; 
    }

    update(vp_size, fov_y) {
        if (vp_size)
            this.vp = vp_size;
        if (fov_y)
            this.fov_y = fov_y;
    }
}

class TrackControl {
    constructor( vp ) {
        this.vp = vp;
        this.orbit_mat = this.current_orbit_mat = this.model_mat = this.current_model_mat = Mat44.ident();
        this.mouse_drag = this.auto_spin = false;
        this.auto_rotate = true;
        this.mouse_start = [0, 0];
        this.mouse_drag_axis = [0, 0, 0];
        this.mouse_drag_angle = 0;
        this.mouse_drag_time = 0;
        this.drag_start_T = this.rotate_start_T = this.time;
        this.auto_rotate_mode = true;
        this.hit = false;
        this.mouse_x = 0;
        this.mouse_y = 0;
        this.active = true;

        this.domElement = document;
        var track = this;
        this.domElement.addEventListener( 'mousedown', function(e) { track.onMouseDown(e) }, false );
        this.domElement.addEventListener( 'mouseup', function(e) { track.onMouseUp(e) }, false );
        this.domElement.addEventListener( 'mousemove', function(e) { track.onMouseMove(e) }, false );
        //this.domElement.addEventListener( 'contextmenu', function(e) { event.preventDefault(); }, false );
        //this.domElement.addEventListener( 'mousewheel', hid_events.onMouseWheel, false );
        //this.domElement.addEventListener( 'DOMMouseScroll', hid_events.onMouseWheel, false ); // firefox
    }

    get time() { return Date.now(); }
        
    get orbit() {
      return this.orbitMatrix;
    }

    get orbitMatrix() {
      return (this.mouse_drag || (this.auto_rotate && this.auto_spin)) ? Mat44.multiply(this.current_orbit_mat, this.orbit_mat) : this.orbit_mat;
    }

    get autoModelMatrix() {
      return this.auto_rotate ? Mat44.multiply(this.current_model_mat, this.model_mat) : this.model_mat;
    }

    attenuation( att_const, att_linear, att_quad ) { this.attenuation = [att_const, att_linear || 0, att_quad || 0]; }

    update(vp_size) {
        if (vp_size)
            this.vp = vp_size;
        let current_T = this.time;
        this.current_model_mat = Mat44.ident()
        if (this.mouse_drag) {
            this.current_orbit_mat = Mat44.rotate(Mat44.ident(), this.mouse_drag_angle, this.mouse_drag_axis);
        } else if (this.auto_rotate) {
            if (this.auto_spin) {
                if (this.mouse_drag_time > 0 ) {
                    let angle = this.mouse_drag_angle * (current_T - this.rotate_start_T) / this.mouse_drag_time;
                    if ( this.attenuation && Math.abs(this.attenuation[0] || 0) > 0.001 )
                        angle /= this.attenuation[0] + this.attenuation[1] * angle + this.attenuation[2] * angle*angle;
                    this.current_orbit_mat = Mat44.rotate(Mat44.ident(), angle, this.mouse_drag_axis);
                }
            } else {
                let auto_angle_x = Util.fract( (current_T - this.rotate_start_T) / 13000.0 ) * 2.0 * Math.PI;
                let auto_angle_y = Util.fract( (current_T - this.rotate_start_T) / 17000.0 ) * 2.0 * Math.PI;
                this.current_model_mat = Mat44.rotateAxis( this.current_model_mat, auto_angle_x, 0 );
                this.current_model_mat = Mat44.rotateAxis( this.current_model_mat, auto_angle_y, 1 );
            }
        }
    }

    changeMotionMode(drag, spin, auto ) {
        let new_drag = drag;
        let new_auto = new_drag ? false : auto;
        let new_spin = new_auto ? spin : false;
        let change = this.mouse_drag != new_drag || this.auto_rotate != new_auto || this.auto_spin != new_spin; 
        if (!change)
            return;
        if (new_drag && !this.mouse_drag) {
            this.drag_start_T = this.time;
            this.mouse_drag_angle = 0.0;
            this.mouse_drag_time = 0;
        }
        if (new_auto && !this.auto_rotate)
            this.rotate_start_T = this.time;
        this.mouse_drag = new_drag; 
        this.auto_rotate = new_auto;  
        this.auto_spin = new_spin;
        this.orbit_mat = Mat44.multiply(this.current_orbit_mat, this.orbit_mat);
        this.current_orbit_mat = Mat44.ident();
        this.model_mat = Mat44.multiply(this.current_model_mat, this.model_mat);
        this.current_model_mat = Mat44.ident();
    }

    onMouseDown( event ) {
        if ( !this.active )
           return;
        this.mouse_x = event.clientX;
        this.mouse_y = event.clientY;
        let rect = gl.canvas.getBoundingClientRect();
        if ( event.clientX < rect.left || event.clientX > rect.right ) return;
        if ( event.clientY < rect.top || event.clientY > rect.bottom ) return;
        this.hit = false;
        if (event.button == 0) { // left button
            if ( this.auto_rotate_mode ) {
                this.mouse_start = [event.clientX, event.clientY]; 
                this.changeMotionMode( true, false, false );
            } else {
                this.hit = true;
            }
        }
    }

    onMouseUp( event ) {
        this.mouse_x = event.clientX;
        this.mouse_y = event.clientY;
        this.hit = false;
        if (event.button == 0 && this.active) { // left button
            if ( this.auto_rotate_mode )
                this.changeMotionMode( false, true, true );
        } else if (event.button == 1 || event.button == 2) {// middle or right button
            //this.changeMotionMode( false, false, !this.auto_rotate );
            //this.auto_rotate_mode = this.auto_rotate;
            this.active = !this.active;
        }
    }

    onMouseMove( event ) {
        if ( !this.active && !this.hit )
            return;
        this.mouse_x = event.clientX;
        this.mouse_y = event.clientY;
        let dx = (event.clientX-this.mouse_start[0]) / this.vp[0];
        let dy = (event.clientY-this.mouse_start[1]) / this.vp[1];
        let len = Math.sqrt(dx*dx + dy*dy);
        if (this.mouse_drag && len > 0) {
            this.mouse_drag_angle = Math.PI*len;
            this.mouse_drag_axis = [-dy/len, 0, dx/len];
            this.mouse_drag_time = this.time - this.drag_start_T;
        }
    }
}

let Util = {
fract : function( val ) { 
  return val - Math.trunc( val );
},
angle : function( deltaTime, intervall ) {
  return this.fract( deltaTime / (1000*intervall) ) * 2.0 * Math.PI;
},
move : function( deltaTime, intervall, range ) {
  var pos = this.fract( deltaTime / (1000*intervall) ) * 2.0
  var pos = pos < 1.0 ? pos : (2.0-pos)
  return range[0] + (range[1] - range[0]) * pos;
},    
ellipticalPosition : function( a, b, angRag ) {
  var a_b = a * a - b * b
  var ea = (a_b <= 0) ? 0 : Math.sqrt( a_b );
  var eb = (a_b >= 0) ? 0 : Math.sqrt( -a_b );
  return [ a * Math.sin( angRag ) - ea, b * Math.cos( angRag ) - eb, 0 ];
}
}

Vec2 = {
    mul_s: function( v, s ) { return [v[0]*s, v[1]*s]; },
    add: function( a, b ) { return [a[0]+b[0], a[1]+b[1]]; },
    sub: function( a, b ) { return [a[0]-b[0], a[1]-b[1]]; },
    dot: function( a, b ) { return a[0]*b[0] + a[1]*b[1]; },
    len: function( v ) { return Math.sqrt( v[0] * v[0] + v[1] * v[1] ); },
    normalize: function( v ) {
        var len = this.len( v );
        return [ v[0] / len, v[1] / len ];
    },
    mix: function( a, b, w ) {
        let wa = 1.0-w;
        let wb = w;
        return [a[0]*wa + b[0]*wb, a[1]*wa + b[1]*wb];
    },
    equal: function( a, b, e ) { return Math.abs(a[0]-b[0]) < e && Math.abs(a[1]-b[1]) < e; }
}

Vec3 = {
    mul_s: function( v, s ) { return [v[0]*s, v[1]*s, v[2]*s]; },
    add: function( a, b ) { return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]; },
    sub: function( a, b ) { return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]; },
    cross: function( a, b ) { return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; },
    dot: function( a, b ) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; },
    len: function( v ) { return Math.sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] ); },
    normalize: function( v ) {
        var len = this.len( v );
        return [ v[0] / len, v[1] / len, v[2] / len ];
    },
    mix: function( a, b, w ) {
        let wa = 1.0-w;
        let wb = w;
        return [a[0]*wa + b[0]*wb, a[1]*wa + b[1]*wb, a[2]*wa + b[2]*wb];
    },
    equal: function( a, b, e ) { return Math.abs(a[0]-b[0]) < e && Math.abs(a[1]-b[1]) < e && Math.abs(a[2]-b[2]) < e; }
}

Mat22 = {
    determinant: function(m) { return m[0]*m[3]-m[1]*m[2]; }
}

Mat33 = {
    ident: function() { return [1,0,0, 0,1,0, 0,0,1]; },
    multiply: function(matA, matB) {
        matC = this.ident();
        for (var i0=0; i0<3; ++i0 )
            for (var i1=0; i1<3; ++i1 )
                matC[i0*3+i1] = matB[i0*3+0]*matA[0*3+i1] + matB[i0*3+1]*matA[1*3+i1] + matB[i0*3+2]*matA[2*3+i1];  
        return matC;
    }
}

Mat44 = {
    ident: function() { return [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]; },
    translate: function( matA, trans ) {
        var matB = matA.slice(0);
        for ( var i = 0; i < 3; ++ i )
            matB[12+i] = matA[i] * trans[0] + matA[4+i] * trans[1] + matA[8+i] * trans[2] + matA[12+i];
        return matB;
    },
    scale: function( matA, scale ) {
        var matB = matA.slice(0);
        for ( var a = 0; a < 4; ++ a )
            for ( var i = 0; i < 3; ++ i )
                matB[a*4+i] = matA[a*4+i] * scale[i];
        return matB;
    },
    rotateAxis: function(matA, angRad, axis) {
        var aMap = [ [1, 2], [2, 0], [0, 1] ];
        var a0 = aMap[axis][0], a1 = aMap[axis][1]; 
        var sinAng = Math.sin(angRad), cosAng = Math.cos(angRad);
        var matB = matA.slice(0);
        for ( var i = 0; i < 3; ++ i ) {
            matB[a0*4+i] = matA[a0*4+i] * cosAng + matA[a1*4+i] * sinAng;
            matB[a1*4+i] = matA[a0*4+i] * -sinAng + matA[a1*4+i] * cosAng;
        }
    return matB;
    },
    rotate: function(matA, angRad, axis) {
        var s = Math.sin(-angRad), c = Math.cos(-angRad);
        var x = axis[0], y = axis[1], z = axis[2]; 
        matB = [
            x*x*(1-c)+c,   x*y*(1-c)-z*s, x*z*(1-c)+y*s, 0,
            y*x*(1-c)+z*s, y*y*(1-c)+c,   y*z*(1-c)-x*s, 0,
            z*x*(1-c)-y*s, z*y*(1-c)+x*s, z*z*(1-c)+c,   0,
            0,             0,             0,             1 ];
        return this.multiply(matA, matB);
    },
    multiply: function(matA, matB) {
    matC = this.ident();
    for (var i0=0; i0<4; ++i0 )
        for (var i1=0; i1<4; ++i1 )
            matC[i0*4+i1] = matB[i0*4+0]*matA[0*4+i1] + matB[i0*4+1]*matA[1*4+i1] + matB[i0*4+2]*matA[2*4+i1] + matB[i0*4+3]*matA[3*4+i1];  
    return matC;
    },
    transformVec4: function(mat, vec) {
        return [
            vec[0] * mat[0*4+0] + vec[1] * mat[1*4+0] + vec[2] * mat[2*4+0] + vec[3] * mat[3*4+0],
            vec[0] * mat[0*4+1] + vec[1] * mat[1*4+1] + vec[2] * mat[2*4+1] + vec[3] * mat[3*4+1],
            vec[0] * mat[0*4+2] + vec[1] * mat[1*4+2] + vec[2] * mat[2*4+2] + vec[3] * mat[3*4+2],
            vec[0] * mat[0*4+3] + vec[1] * mat[1*4+3] + vec[2] * mat[2*4+3] + vec[3] * mat[3*4+3] ]
    },
    inverse: function( m ) {
        
        let vec4_add = function( a, b ) { return [ a[0]+b[0], a[1]+b[1], a[2]+b[2], a[3]+b[3] ]; }
        let vec4_sub = function( a, b ) { return [ a[0]-b[0], a[1]-b[1], a[2]-b[2], a[3]-b[3] ]; }
        let vec4_mul = function( a, b ) { return [ a[0]*b[0], a[1]*b[1], a[2]*b[2], a[3]*b[3] ]; }
        let vec4_scale = function( a, s ) { return [ a[0]*s, a[1]*s, a[2]*s, a[3]*s ]; }

        let Coef00 = m[2*4+2] * m[3*4+3] - m[3*4+2] * m[2*4+3];
        let Coef02 = m[1*4+2] * m[3*4+3] - m[3*4+2] * m[1*4+3];
        let Coef03 = m[1*4+2] * m[2*4+3] - m[2*4+2] * m[1*4+3];    
        let Coef04 = m[2*4+1] * m[3*4+3] - m[3*4+1] * m[2*4+3];
        let Coef06 = m[1*4+1] * m[3*4+3] - m[3*4+1] * m[1*4+3];
        let Coef07 = m[1*4+1] * m[2*4+3] - m[2*4+1] * m[1*4+3];   
        let Coef08 = m[2*4+1] * m[3*4+2] - m[3*4+1] * m[2*4+2];
        let Coef10 = m[1*4+1] * m[3*4+2] - m[3*4+1] * m[1*4+2];
        let Coef11 = m[1*4+1] * m[2*4+2] - m[2*4+1] * m[1*4+2];   
        let Coef12 = m[2*4+0] * m[3*4+3] - m[3*4+0] * m[2*4+3];
        let Coef14 = m[1*4+0] * m[3*4+3] - m[3*4+0] * m[1*4+3];
        let Coef15 = m[1*4+0] * m[2*4+3] - m[2*4+0] * m[1*4+3];   
        let Coef16 = m[2*4+0] * m[3*4+2] - m[3*4+0] * m[2*4+2];
        let Coef18 = m[1*4+0] * m[3*4+2] - m[3*4+0] * m[1*4+2];
        let Coef19 = m[1*4+0] * m[2*4+2] - m[2*4+0] * m[1*4+2];   
        let Coef20 = m[2*4+0] * m[3*4+1] - m[3*4+0] * m[2*4+1];
        let Coef22 = m[1*4+0] * m[3*4+1] - m[3*4+0] * m[1*4+1];
        let Coef23 = m[1*4+0] * m[2*4+1] - m[2*4+0] * m[1*4+1];
        
        let Fac0 = [Coef00, Coef00, Coef02, Coef03];
        let Fac1 = [Coef04, Coef04, Coef06, Coef07];
        let Fac2 = [Coef08, Coef08, Coef10, Coef11];
        let Fac3 = [Coef12, Coef12, Coef14, Coef15];
        let Fac4 = [Coef16, Coef16, Coef18, Coef19];
        let Fac5 = [Coef20, Coef20, Coef22, Coef23];
        
        let Vec0 = [ m[1*4+0], m[0*4+0], m[0*4+0], m[0*4+0] ];
        let Vec1 = [ m[1*4+1], m[0*4+1], m[0*4+1], m[0*4+1] ];
        let Vec2 = [ m[1*4+2], m[0*4+2], m[0*4+2], m[0*4+2] ];
        let Vec3 = [ m[1*4+3], m[0*4+3], m[0*4+3], m[0*4+3] ];
        
        let Inv0 = vec4_add( vec4_sub( vec4_mul(Vec1, Fac0), vec4_mul(Vec2, Fac1) ), vec4_mul( Vec3, Fac2 ) );
        let Inv1 = vec4_add( vec4_sub( vec4_mul(Vec0, Fac0), vec4_mul(Vec2, Fac3) ), vec4_mul( Vec3, Fac4 ) );
        let Inv2 = vec4_add( vec4_sub( vec4_mul(Vec0, Fac1), vec4_mul(Vec1, Fac3) ), vec4_mul( Vec3, Fac5 ) );
        let Inv3 = vec4_add( vec4_sub( vec4_mul(Vec0, Fac2), vec4_mul(Vec1, Fac4) ), vec4_mul( Vec2, Fac5 ) );
        
        let SignA = [+1.0, -1.0, +1.0, -1.0];
        let SignB = [-1.0, +1.0, -1.0, +1.0];
        let Inverse = [ vec4_mul(Inv0, SignA), vec4_mul(Inv1, SignB), vec4_mul(Inv2, SignA), vec4_mul(Inv3, SignB) ];
        
        let Row0 = [Inverse[0][0], Inverse[1][0], Inverse[2][0], Inverse[3][0] ];
        
        let Dot0 = [Row0[0], Row0[1], Row0[2], Row0[3] ];
        Dot0 = vec4_mul( Dot0, [ m[0], m[1], m[2], m[3] ] );
        let Dot1 = (Dot0[0] + Dot0[1]) + (Dot0[2] + Dot0[3]);
        
        let OneOverDeterminant = 1 / Dot1;

        var res = this.ident();  
        for ( var inx1 = 0; inx1 < 4; inx1 ++ ) {
            for ( var inx2 = 0; inx2 < 4; inx2 ++ )
                res[inx1*4+inx2] = Inverse[inx1][inx2] * OneOverDeterminant;
        }
        return res;
    }
}

function render(deltaMS){
    app.draw(deltaMS);
    requestAnimationFrame(render);
}

let app = new App();
requestAnimationFrame(render);


})();
</script>

</div>
</body>
</html>